# encoding=utf-8

"""Arcus重构

    重构一期：
       - 一期任务主要在于摘抄旧代码的部分，并重新进行划分组合
       - 根据业务的主要逻辑，项目分为了以下几个模块：
        - seg_common: 存放interface, utils, common service
        - seg_cuda: 算法加速模块/混编封装
        - seg_quartz: 定时任务
        - seg_system: 系统核心业务存放位置
        - seg_test: 业务的单元测试
        - seg_ui: uni-app重构的三端ui界面
"""
import json
import os
import copy
import traceback
import requests
from flask_cors import CORS
from flask import Flask, make_response
from flask import jsonify, request
from flask_apscheduler import APScheduler
from concurrent.futures import ThreadPoolExecutor

import seg_quartz
from seg_common.MiddleWareService import RedisService
from seg_common.Paser import ParserService
from seg_common.LoggingService import ConsoleService
from seg_common.Exception import ExceptionCallBack
from seg_common.TaskQueue import FlaskPredictWaitTaskQueue
from seg_common.Identity import UUIDTools
from seg_system import Multibatch
from seg_system import ApplicationConfig
from seg_system import Vascular
from seg_system.Clean import FileCleanService as FileDeleteService
from seg_system.Download import DownloadDirFactory
from seg_system.Download import DownloadService
from seg_system.Download import DownloadExcelService
from seg_system.Multibatch import LongTaskMonitorService
from seg_system.LongTaskStatusUpload import MultiStatusUploadImpl


class ControllerCommon:
    """ControllerCommon
        - 本类可以看成常量类，记录主程序的所有常量信息
    """
    # flask主程序
    app = Flask(__name__, template_folder='./templates',
                static_folder='./templates', static_url_path='')
    # flask计时器
    aps_scheduler = APScheduler()
    # TODO 读取tmp目录用，以后整合到application_config里面
    basepath = os.path.dirname(__file__)  # 实际上还是代码没重写完毕，本来应该记录在配置文件里

    # 常规的工具部分
    RCParserTool = ParserService.ParserTools()  # 解析并转换系统的Request信息， Request Common Paser Tool
    RedisTools = RedisService()  # 本代码中redis工具类的实例化

    # uuid
    uuid = UUIDTools.get_uuid()

    # 通用的静态函数
    # 用于POST系列的DirMakeFactory初始化
    @staticmethod
    def saver_copy_and_init(copy_from, r, path: str = None):
        c = copy.deepcopy(copy_from)
        c.path_init_with_request(r, path)
        return c

    # 用于GET系列的DirMakeFactor的替代方案
    @staticmethod
    def saver_copy_and_init_get(copy_service, u_name, token, path: str = None):
        c = copy.deepcopy(copy_service)
        c.temp_dir_create(save_dir=path, user_name=u_name, token=token)
        return c


class FileController:
    # TODO 文件服务的相关接口

    # 用于zip文件的下载
    FCDownloadZipTool = DownloadService()
    FCDownloadSaveService = DownloadDirFactory()  # 配套的路径计算类

    # 用于把文件归档为excel
    FCDownloadExcelTool = DownloadExcelService()

    # 路径计算以及初始化类
    SaveService = Multibatch.SaveService()  # 初始化图片的上传信息
    VascularSaveService = Vascular.VascularDirFactory()  # 初始化vascular相关模块的路径
    DownloadSaveService = DownloadDirFactory()  # 初始化临时目录相关的路径

    # 用于文件删除服务
    DelService = FileDeleteService()

    @staticmethod
    @ControllerCommon.app.route('/app/file/delete', methods=['POST'])
    async def delete_file():
        img_name = ControllerCommon.RCParserTool.parser_img_name_from_request(request)
        seg_saver = ControllerCommon.saver_copy_and_init(FileController.SaveService, request, ControllerCommon.basepath)
        vascular_saver = ControllerCommon.saver_copy_and_init(FileController.VascularSaveService, request,
                                                              ControllerCommon.basepath)
        return FileController.DelService.clean_main(img_name, seg_saver, vascular_saver)

    @staticmethod
    @ControllerCommon.app.route('/app/multiupload', methods=['POST'])
    async def multi_batch_upload():
        """
            文件上传接口

            @TODO 没有关注到后续的文件夹扩展，所以看起来比较奇怪
            @TODO 只使用了文件夹，又不允许多次创建，所以更新代码，会导致旧token目录结构不会更新，需要采用数据库解偶
        :return:
        """
        # 创建角膜神经分割文件夹
        result, message = FileController.SaveService.img_save(request, ControllerCommon.basepath)

        # 创建用户临时目录
        result, m_2 = FileController.DownloadSaveService.path_init_with_request(request, ControllerCommon.basepath)
        message.append(m_2[0])

        # 创建神经量化评估文件夹
        result, m_1 = FileController.VascularSaveService.path_init_with_request(request, ControllerCommon.basepath)
        message.append(m_1[0])

        return jsonify({'result': result, 'message': message})

    @staticmethod
    @ControllerCommon.app.route('/app/download/excel', methods=['GET'])
    async def download_excel():
        user_name, token = ControllerCommon.RCParserTool.parser_name_token_from_request(request, 'GET')
        img_list, all_img = ControllerCommon.RCParserTool.paser_img_list_from_request(request)
        language = request.args.get('language', 'CHI')

        if not ControllerCommon.RCParserTool.arg_is_not_none([user_name, token, img_list]):
            return {'code': '501', 'message': 'argument valid error'}

        # 需要借用predict_get接口里面SaveService，VascularSaveService进行扫盘
        seg_saver = ControllerCommon.saver_copy_and_init_get(
            FileController.SaveService, user_name, token, ControllerCommon.basepath)
        vascular_saver = ControllerCommon.saver_copy_and_init_get(
            FileController.VascularSaveService, user_name, token, ControllerCommon.basepath)

        return FileController.FCDownloadExcelTool.make_excel(seg_saver, vascular_saver, img_list, all_img,
                                                             ApplicationConfig.SysLanguage.EXCEL_EXPORT_DICT[language])

    @staticmethod
    @ControllerCommon.app.route("/app/download/zip", methods=["GET"])
    def multi_batch_download_zip():
        """
            重写的download部分，修改为Get接口，适应不同的浏览器下载需求
        :return:
        """
        user_name, token = ControllerCommon.RCParserTool.parser_name_token_from_request(request, 'GET')
        language = request.args.get('language', 'CHI')
        if not ControllerCommon.RCParserTool.arg_is_not_none([user_name, token]):
            return {'code': '501', 'message': 'argument valid error'}

        download_saver = ControllerCommon.saver_copy_and_init_get(
            FileController.FCDownloadSaveService, user_name, token, ControllerCommon.basepath)

        return FileController.FCDownloadZipTool.download_zip_file_with_label(
            download_saver, user_name, token, LAG_DICT=ApplicationConfig.PathConfig.DOWNLOAD_LANGUAGE[language])

    @staticmethod
    @ControllerCommon.app.route('/tmp/<path:file>', methods=['GET'])
    async def show_photo(file):
        if request.method == 'GET':
            if not file is None:
                try:
                    image_data = open(f'tmp/{file}', "rb").read()
                except Exception as e:
                    print(traceback.format_exc())
                    image_data = open(f'tmp/loading.png', "rb").read()

                response = make_response(image_data)
                response.headers['Content-Type'] = 'image/png'
                return response

    # 历史保留，可以用于调试，一般不使用
    @staticmethod
    @ControllerCommon.app.route("/app/multidownload", methods=["POST"])
    def multi_batch_download():
        """
           旧文件下载接口，用于调试，保留
       :return:
       """
        uName, token = ControllerCommon.RCParserTool.parser_name_token_from_request(request)
        download_saver = ControllerCommon.saver_copy_and_init(
            FileController.FCDownloadSaveService, request, ControllerCommon.basepath)
        return FileController.FCDownloadZipTool.download_zip_file(download_saver, uName, token)

    @staticmethod
    @ControllerCommon.app.route("/app/multi_download_label", methods=["POST"])
    def multi_batch_download_with_label():
        """
            将文件夹重命名（中文/英文）后返回下载

            @TODO 后续会提供过滤部分信息的接口
        :return:
        """
        uName, token, language = ControllerCommon.RCParserTool.parser_download_param_from_request(request)
        download_saver = ControllerCommon.saver_copy_and_init(
            FileController.FCDownloadSaveService, request, ControllerCommon.basepath)
        return FileController.FCDownloadZipTool.download_zip_file_with_label(
            download_saver, uName, token, LAG_DICT=ApplicationConfig.PathConfig.DOWNLOAD_LANGUAGE[language])


class InferController:
    """tacom develop
        note: 2022.7.26 -
        
       TODO: 2022.12.9 开始重写接口划分，初步分为common, file, infer, monitor, scheduler
    """
    # thread pool(控制批量化运算的实例数量)
    executor = ThreadPoolExecutor(max_workers=ApplicationConfig.SystemConfig.NUM_PREDICT_USER)

    # infer过程中的具体redis缓存设置规则
    TaskStateUploadService = MultiStatusUploadImpl()  # 任务状态监控

    # 执行期
    BatchPredictService = Multibatch.BatchPredictService()  # seg系列的执行器
    VascularPredictService = Vascular.VascularService()  # vascular系列的执行器

    # 等待队列
    _predictTaskQueue = FlaskPredictWaitTaskQueue()

    # tmp var
    future_obj = None

    @staticmethod
    @ControllerCommon.app.route('/app/preditstart', methods=['POST'])
    def multi_batch_start():
        """
            开启线程池后，所有报错直接被拦截，不显示，需要注意
            压力测试的时候，一个都炸，所以不要设置多个worker

            @TODO 这个是批量化的处理接口，后续实现单文件的处理流程
        :return:
        """
        config = ControllerCommon.RCParserTool.parser_config_from_upload_json(request)

        if 'result' in config:
            # 异常返回
            return jsonify(config)

        # 深复制，减少路由并发异常
        saver = ControllerCommon.saver_copy_and_init(FileController.SaveService, request, ControllerCommon.basepath)
        vascular_saver = ControllerCommon.saver_copy_and_init(FileController.VascularSaveService, request,
                                                              ControllerCommon.basepath)

        uName, token = ControllerCommon.RCParserTool.parser_name_token_from_request(request)
        redis_id = ControllerCommon.RedisTools.make_redis_key(uName, token)

        # 非主线程,无法传递request
        if InferController.future_obj is None or InferController.future_obj.done():
            InferController.TaskStateUploadService.update_seq(redis_id)
            future = InferController.executor.submit(
                InferController.BatchPredictService.predict_start,
                saver, ControllerCommon.basepath, uName, token, config,
                vascular_saver=vascular_saver,
                vascular_predictor=InferController.VascularPredictService,
                redis_id=redis_id
            )
            ExceptionCallBack.predict_pool_exception_info[id(future)] = redis_id
            InferController.future_obj = future
            future.add_done_callback(ExceptionCallBack.threading_pool_callback)
            return jsonify({'result': True, 'message': 'long task start',
                            'id': redis_id})
        else:
            return jsonify({'result': False, 'message': 'a request is dealing, waiting!!'})

    """
        Developer: tacom
        Data: 2023.1.23
        Description: 
            为达成快速迭代的目标，预测主流成重写如下：
                1. 增加等待队列，但不存储进数据库，丢失即重新用户发起请求排队
                2. 增加/app/predict/query, 查询当前用户的排队进度，如果返回0，即开始处理，否则前台显示等待号码
                3. 队列的出栈暂时规划为monitor设计模式
    """

    @staticmethod
    @ControllerCommon.app.route("/app/predict/queue/query", methods=['GET'])
    def tmp_queue_query():
        # TODO 职能需要迁移到JAVA端
        redis_id = request.args.get('redis_id', None)
        if redis_id is None:
            return {
                'result': False,
                'message': 'args error'
            }

        key = ApplicationConfig.SystemConfig.REDIS_TASK_QUEUE_PRE + redis_id
        value = InferController._predictTaskQueue.task_queue_uploader.key_get(key)
        if value == {}:
            return {
                'result': False,
                'message': 'value is empty'
            }

        return {
            'result': True,
            'message': value['location']
        }

    @staticmethod
    @ControllerCommon.app.route("/app/predict/v1", methods=['POST'])
    def tmp_predict_v1():
        # TODO 需要重写为tensorrt版本
        # TODO redis_id部分需要组合前缀，方便缓存清理

        # Loader and init
        config = ControllerCommon.RCParserTool.parser_config_from_upload_json(request)
        if 'result' in config:
            return jsonify(config)

        saver = ControllerCommon.saver_copy_and_init(FileController.SaveService, request, ControllerCommon.basepath)
        vascular_saver = ControllerCommon.saver_copy_and_init(
            FileController.VascularSaveService, request, ControllerCommon.basepath
        )

        uName, token = ControllerCommon.RCParserTool.parser_name_token_from_request(request)
        redis_id = ControllerCommon.RedisTools.make_redis_key(uName, token)

        # en queue
        each_queue_task = {
            'config': config,
            'saver': saver,
            'vascular_saver': vascular_saver,
            'uName': uName,
            'token': token,
            'redis_id': redis_id
        }

        InferController._predictTaskQueue.task_in(each_queue_task)

        return jsonify({'result': True, 'message': 'task in queue', 'id': redis_id})

    """
        Date: 2023.1.23
        Description:
            此处为规划的预测主流程：
                1. 重写部分模型为Tensorrt版本
                2. 增加request里POST的内容，包括predict_list = ['*'] 类似的格式，支持指定文件的预测
                    - 此操作适配单图/多图的不同请求重新检测
                3. 增加更加精细的进度追踪，从而可以给前端返回更加准确的进度信息
                4. 增加websocket，用于连接Java,等待Java的指挥
    """

    """
        Date: 2023.5.26
        Description:
            邻近结题,暂时无法实现最大化的系统调度,为实现1000人共用系统的目标
            开始设计简单的调度器,来尽可能的满足这个目标
    """

    @staticmethod
    @ControllerCommon.app.route('/app/predict/isBusy', methods=['GET'])
    async def system_busy_query():
        # 此接口有三个作用, 一个是表明这个实例还活着, 另一个输出队列长度, 用于负载分配
        # 最后一个用于输出阿里云的端口映射, 用于服务器精确发送消息
        return jsonify({
            "uuid": ControllerCommon.uuid,
            "isBusy": len(InferController._predictTaskQueue.task_queue),
            "ip": ApplicationConfig.URLConfig.BASE_URL,
            "port": ApplicationConfig.URLConfig.BASE_PORT
        })


class MonitorController:
    # TODO 存储监控相关的接口信息

    # Redis缓存
    TaskMonitorService = LongTaskMonitorService()  # 给前台返回redis缓存信息

    @staticmethod
    @ControllerCommon.app.route('/app/longTaskStateGet', methods=['POST'])
    async def get_task_state():
        """
            通过redis_id获取长任务的状态信息
        :return:
        """
        redis_id, result = ControllerCommon.RCParserTool.parser_redis_id_from_request(request)
        message = MonitorController.TaskMonitorService.get_all(redis_id)
        return jsonify({'result': result, 'message': message})

    @staticmethod
    @ControllerCommon.app.route("/app/predict_get", methods=["POST"])
    async def predict_get():
        """
            saver: 根据Application_config得到的文件url路径
            saver_with_path: 计算得到的缓存文件路径，用于检验文件是否正确生成
        :return:
        """
        img_name = ControllerCommon.RCParserTool.parser_img_name_from_request(request)

        saver = ControllerCommon.saver_copy_and_init(FileController.SaveService, request)
        saver_with_path = ControllerCommon.saver_copy_and_init(FileController.SaveService, request,
                                                               ControllerCommon.basepath)

        vascular_saver = ControllerCommon.saver_copy_and_init(FileController.VascularSaveService, request)
        vascular_saver_with_path = ControllerCommon.saver_copy_and_init(FileController.VascularSaveService, request,
                                                                        ControllerCommon.basepath)

        return ControllerCommon.RCParserTool.parse_from_image_name(
            img_name, saver, saver_with_path,
            vascular_saver=vascular_saver,
            vascular_saver_with_path=vascular_saver_with_path
        )


class SchedulerController:
    # TODO 定时任务接口，需要换执行框架，指定周几，xx小时执行
    FileCleanService = seg_quartz.FileCleanService()  # 清空上传的缓冲区

    TimeRecorderSaveService = seg_quartz.TimeRecorderSaveService()

    @staticmethod
    # @ControllerCommon.aps_scheduler.task('interval', id='clean_job', days=7)
    def file_clean_start():
        """
            @TODO bug, flask的aps无法设置周几执行
        :return:
        """
        SchedulerController.FileCleanService.clean_multi_batch_cache_utils_latest(ControllerCommon.basepath)

    @staticmethod
    # @aps_scheduler.task('interval', id='time_record_save', minutes=1)
    def time_record_start():
        SchedulerController.TimeRecorderSaveService.time_record()

    @staticmethod
    # @aps_scheduler.task('interval', id='update_console_log', minutes=1)
    def update_console_log():
        ConsoleService.make_logger()

    @staticmethod
    @ControllerCommon.app.route('/app/scheduler/heart', methods=['GET'])
    async def system_heart():
        send_msg = {
            "uuid": ControllerCommon.uuid,
            "ip": ApplicationConfig.URLConfig.BASE_URL,
            "port": ApplicationConfig.URLConfig.BASE_PORT
        }
        header_dict = {"Content-Type": "application/json; charset=utf8"}
        dst_url = "http://{}:{}/arcus/instanceReg".format(ApplicationConfig.URLConfig.SPRING_URL,
                                                          ApplicationConfig.URLConfig.SPRING_PORT)

        try:
            conn = requests.post(dst_url, data=json.dumps(send_msg), headers=header_dict)
            return jsonify({"result": int(conn.text), "code": conn.status_code})
        except:
            return jsonify({"result": -1, "code": 500})


if __name__ == "__main__":
    """
        flask高并法部署方案:
            1. pywsgi:
                - 使用了monkey补丁也在接受别的机子请求的时候失败
                - 从测试来看,并法性能超过了gunicorn(上传500张图片速度上)
                - 但是只在同一台电脑上测试通过
            2. gunicorn:
                - refs: https://blog.csdn.net/qq_39691492/article/details/122344712
                - gunicorn -w 8 --threads 12 -b 0.0.0.0:50001 main:app --reload
                - gunicorn启动8个实例, 对外只表现为一个50001端口的对象, 
                  每个实例共用12个线程, 启动的对象是main.py中的app名字的flask对象
                - 一致性检验结果:
                    1. 对于每台机器, 通常预测线程为1, gunicorn的使用导致预测线程异常
                       具体表现为,等待队列不启动(实际上都不再同一个进程里), 两次重复的预测请求
                       直接导致了Titan XP内存不足, 程序爆炸
                    2. 
                - 结论: 
                    由于设计以后的文件存储位置不一致, 预测请求需要统一, websocket实例id等问题
                    暂时不使用并法框架
                    
                    1. 预测的部分应该想办法同一请求后, 在使用此框架部署,否则一个用户同时请求两次
                       就可能分配到不同的实例中预测,导致资源浪费
                    2. 对于以后的websocket部分,可能调度麻烦
                    3. 对于数据的记录部分,则需要进行划分,否则造成记录覆盖
    """

    CORS(ControllerCommon.app, resources=r'/*')
    ControllerCommon.aps_scheduler.init_app(ControllerCommon.app)
    ControllerCommon.aps_scheduler.start()
    ControllerCommon.app.config['JSON_AS_ASCII'] = False

    if ApplicationConfig.SystemConfig.DEBUGGER:
        ControllerCommon.app.run(
            **ApplicationConfig.SystemConfig.SERVER_MODE_DICT[ApplicationConfig.SystemConfig.SERVER_MODE]
        )
    else:
        # TODO gevent can't run by frpc transfer?
        #      but can run fast in local network
        ControllerCommon.app.run(
            **ApplicationConfig.SystemConfig.SERVER_MODE_DICT[ApplicationConfig.SystemConfig.SERVER_MODE]
        )
        # from gevent import pywsgi
        # server = pywsgi.WSGIServer(
        #     (ApplicationConfig.SystemConfig.SERVER_HOST, ApplicationConfig.SystemConfig.SERVER_PORT),
        #     ControllerCommon.app
        # )
        # server.serve_forever()
